package time.providers.sql.query;
import haxe.macro.Expr;
import time.compiler.query.Data;
import time.compiler.query.Solver;
import time.compiler.types.Data;
import time.compiler.types.TypeBuilder;
import time.providers.sql.query.Data;
import time.providers.sql.query.Solvers;

using time.compiler.types.Data;

typedef ParserVariable = {
	var variable(default, null):String;
	//the expr is read-only
	var expr(default, null):SqlExpr;
};

class SqlGenerator extends time.providers.base.compiler.Generator
{
	private var query:SqlQuery;
	private var joins:Array<{alias:String, join:SqlJoin}>;
	private var selectCandidate:SqlSelect;
	
	private var vars:Array< Hash<ParserVariable> >;
	private var vnames:Hash<{ sourceAlias:String }>;
	private var vnamesCount:Int;
	
	private var aliasCount:Int;
	private var aliases:Hash<{declaredLevel:Int, source:SqlJoinSource, sourceAlias:Null<String>, levels:IntHash<Array<String>>}>;
	
	private var levels:Array<SqlQuery>;
	
	private var cache:Hash<SqlSolver>;
	public var pos(default, null):Null<Position>;
	
	public function new()
	{
		super();
	}
	
	public override function parse(query:QueryExpr) : Expr
	{
		if (cache == null)
		{
			cache = new Hash();
			for (s in this.solvers)
				cache.set(s.cname, cast(s, SqlSolver));
		}
		
		query = null;
		joins = null;
		
		vars = [];
		vnames = new Hash();
		vnamesCount = 0;
		
		aliasCount = 0;
		aliases = new Hash();
		
		levels = [];
	}
	
	public function pushBlock()
	{
		vars.push(new Hash());
	}
	
	public function popBlock()
	{
		vars.pop();
	}
	
	public function lookup(name:String)
	{
		var len = vars.length;
		for (i in 0...len)
		{
			var cur = vars[len - i];
			if (cur != null && cur.exists(name))
				return cur.get(name);
		}
		
		return error("Cannot find variable named " + name);
	}
	
	public function error(msg:String, ?pos:Null<Position>):Dynamic
	{
		throw msg;
		return null;
	}
	
	private function getAlias(src:SqlJoinSource, sourceAlias:Null<String>):String
	{
		var ret = "t" + (aliasCount++) + "_";
		this.aliases.set(ret, { declaredLevel:getLevel(), source:src, sourceAlias:sourceAlias, levels:new IntHash() } );
		
		return ret;
	}
	
	private function getSolver(cname:String):SqlSolver
	{
		var g = cache.get(cname);
		if (g != null)
			return g;
			
		var s = cast(Type.createInstance(Type.resolveClass(cname), []), SqlSolver);
		cache.set(cname, s);
		return s;
	}
	
	private function getLevel() : Int
	{
		return levels.length - 1;
	}
	
	private function newSqlQuery(entryPoint:SqlJoinSource, sourceAlias):String
	{
		var ep = {
			alias:getAlias(entryPoint, sourceAlias),
			from:entryPoint,
			joins:[]
		};
		
		this.joins = ep.joins;
		
		var level = levels.length;
		var ret = {
			select:null,
			from:[ep],
			where:null,
			aggregator:null,
			having:null,
			order:null,
			limit:null,
			level:level
		};
		
		levels.push(ret);
		
		this.query = ret;
		
		return ep.alias;
	}
	
	public function newVar(p:ParserVariable) : SqlExpr
	{
		vars[this.vars.length - 1].set(p.variable, p);
		
		return p.expr;
	}
	
	private function getVariable(expr:SqlExpr) : Variable
	{
		switch(expr)
		{
			case SValue(v):
				switch(v)
				{
					case VVariable(v):
						return v;
					default:
						error("Expecting VVarible, got " + v);
				}
			default:
				error("Expecting VVariable, got " + expr);
		}
	}
	
	/**
	 *  Adds a Value Type column to the variable
	 */
	private function addVTColumn(v:Variable, column:String, isStruct:Bool) : Variable
	{
		return switch(v)
		{
			case VStruct(alias, columns, valueColumn):
				if (valueColumn != null)
				{
					error("Cannot add a column accessor for a closed struct access (latest value column is " + valueColumn + ")" );
				}
				
				if (isStruct)
				{
					var c = columns.copy();
					c.push(column);
					VStruct(alias, c, null);
				} else {
					VStruct(alias, columns, column)
				}
			case VAlias(alias):
				if (isStruct)
					VStruct(alias, [column], null);
				else
					VColumn(alias, column);
			case VName(_), VColumn(_,_):
				error("A SQL variable cannot have a column accessor added to it");
		}
	}
	
	//gets the alias of the new table, and then adds the join, and returns the alias
	private function getJoin( leftAlias:String, leftField:String, rightField:String, joinOp:SqlJoinOp, tbl:SqlJoinSource ) : String
	{
		var alias = getAlias(tbl, leftAlias);
		
		this.joins.push({alias:alias, join:SClassicJoin(
			VColumn(leftAlias, leftField),
			VColumn(alias, rightField)
		)});
		
		return alias;
	}
	
	private function getExprAlias(e:SqlExpr) : String
	{
		return switch(getVariable(e))
		{
			case VAlias(a):
				a;
			default:
				error("An alias expression is required; But a " + getVariable(e) + " was passed instead");
		}
	}
	
	private function getPrimaryKeyField(t:DBType):String
	{
		return switch(t.follow())
		{
			case DBInst(i):
				i.idField
			case DBEnum(e):
				
		}
	}
	
	/**
	 * Evaluates all descending expressions, and reduces them to only SqlExpr's
	 * All descend operations result in another join config (if the join doesn't already exist (TODO))
	 * And redirect call exprs to solvers (and get their returning value)
	 * 
	 * @param	query
	 * @return
	 */
	public function parseToExpr(query:QueryExpr):SqlExpr
	{
		var lastPos = this.pos;
		this.pos = query.pos;
		var ret = switch(query.expr)
		{
			case QVar(name, expr):
				var v = {variable:name, expr:parseToExpr(expr)};
				newVar(v);
				
				SValue(VNull); //var is always void
			case QDescend(leftExpr, field, mk):
				var leftSql = parseToExpr(leftExpr);
				var leftAlias = getExprAlias(leftSql); //always the left side of a QDescend MUST be a Sql variable
				var rightExprType = query.t;
				switch(mk)
				{
					case MValueType:
						SValue(VVariable(addVTColumn( leftVar, field, !query.t.isBasic() )));
					case MRelation(rel):
						switch(rel)
						{
							//TODO support multiple primary keys
							case ParentToChild:
								var joinAlias = getJoin(
									leftAlias,
									field,
									getPrimaryKeyField(rightExprType),
								LeftJoin, STable(rightExprType));
								
								SValue(VVariable(VAlias(joinAlias)));
							case ParentToChildren:
								rightExprType = switch(rightExprType.follow())
								{
									case DBToMany(t): t;
									default: throw Assert;
								}
								
								var joinAlias = getJoin(
									leftAlias,
									getPrimaryKeyField(leftExpr.t),
									"__ref_parent__", //TODO take off this hard-coded behaviour
								LeftJoin, STable(rightExprType));
								
								SValue(VVariable(VAlias(joinAlias)));
							case ChildToParent(parentField):
								var joinAlias = getJoin(
									leftAlias,
									getPrimaryKeyField(leftExpr.t),
									parentField,
								LeftJoin, STable(rightExprType));
								
								SValue(VVariable(VAlias(joinAlias)));
							case ChildrenToParent(parentField):
								var joinAlias = getJoin(
									leftAlias,
									"__ref_parent__",
									getPrimaryKeyField(rightExprType),
								LeftJoin, STable(rightExprType));
								
								SValue(VVariable(VAlias(joinAlias)));
						}
					case MReference(to, opt):
						//getJoin( leftAlias:String, leftField:String, rightField:String, joinOp:SqlJoinOp, tbl:SqlJoinSource ) : String
						var dbpath = switch(expr.t.follow())
						{
							case DBInst(i): i.dbPath;
							default: throw Assert; //only classes can be referenced
						}
						
						//TODO take care of this hard-coded value
						var refName = dbpath + "___rel___" + field;
						
						var refAlias = getJoin(
							leftAlias,
							getPrimaryKeyField(leftExpr.t),
							dbpath,
						LeftJoin, SInternalTable(refName));
						var alias = getJoin(
							refAlias,
							field + "_ref",
							getPrimaryKeyField(to),
						LeftJoin, STable(to));
						
						SValue(VVariable(VAlias(alias)));
					case MBackReference(to, refField, opts):
						var dbpath = switch(to.follow())
						{
							case DBInst(i): i.dbPath;
							default: throw Assert; //only classes can be referenced
						}
						
						//TODO take care of this hard-coded value
						var refName = dbpath + "___rel___" + refField;
						
						var refAlias = getJoin(
							leftAlias,
							getPrimaryKeyField(leftExpr.t),
							refField + "_ref",
						LeftJoin, SInternalTable(refName));
						var alias = getJoin(
							refAlias,
							dbpath,
							getPrimaryKeyField(to),
						LeftJoin, STable(to));
						
						SValue(VVariable(VAlias(alias)));
					case MSerialized:
						throw Assert;
				}
				
			case QBinop(op, e1, e2):
				SBinop(op, parseToExpr(e1), parseToExpr(e2));
			case QUnop(op, e):
				SUnop(op, parseToExpr(e));
			case QCall(e1, s, ecall, solver):
				var e1q = parseToExpr(e1);
				var s = getSolver(solver);
				s.handleCall(e1q, s, ecall, query.t, this);
			case QBuiltin(op, ecall):
				//TODO implement builtin
				throw NotImplemented;
			case QConst(c):
				switch(c)
				{
					case QCExternalVar(name): SValue(VExternalVar(name));
					case QCVar(name): lookup(name).expr;
					case QCInt(i): SValue(VInt(i));
					case QCFloat(f): SValue(VFloat(f));
					case QCString(s): SValue(VString(s));
					case QCBool(b): SValue(VBool(b));
					case QCNull: SValue(VNull);
					case QCType(_): throw Assert;
				}
			case QBlock(bl):
				var v = null;
				pushBlock();
				for (b in bl)
				{
					v = parseToExpr(b);
				}
				popBlock();
				
				v;
			case QPar(p):
				SPar(parseToExpr(p));
			case QSelect(ep):
				if (this.query != null)
				{
					error("Multiple entry points!");
				}
				
				var alias = newSqlQuery(STable(TypeBuilder.getData(ep)), null);
				SValue(VVariable(VAlias(alias)));
			case QFunction(_): throw Assert;
		}
		
		this.pos = lastPos;
		return ret;
	}
	
	private function exprUsesVars(expr:SqlExpr):Bool
	{
		return switch(expr)
		{
			case SBinop(_, e1, e2): exprUsesVVar(e1) || exprUsesVVar(e2);
			case SUnop(_, e), SPar(e): exprUsesVVar(e);
			case SValue(v):
				switch(v)
				{
					case VVariable(v):
						switch(v)
						{
							case VName(_): true;
							default: false;
						}
					default: false;
				}
		}
	}
	
	//recursively separate AND expressions
	private function separateAndExprs(e:SqlExpr, agg:Array<SqlExpr>):Array<SqlExpr>
	{
		switch(e)
		{
			case SBinop(op, e1, e2):
				switch(op)
				{
					case QOpBoolAnd:
						separateAndExprs(e1, agg);
						separateAndExprs(e2, agg);
					default: agg.push(e); agg;
				}
			default: agg.push(e); agg;
		}
	}
	
	public function addCondition(cond:SqlExpr) : Void
	{
		//if there is an aggregator, we need to check if the condition will be at HAVING side or WHERE.
		//as a rule of thumb, only exprs that use GROUP BY (so have VName variables) will have to be in the having side
		
		//TODO add check to filter on the ON join clause
		if (this.query.aggregator != null)
		{
			var exprs = separateAndExprs(cond);
			for (e in exprs)
			{
				if (exprUsesVars(e))
				{
					if (query.having == null)
						query.having = e;
					else
						query.having = SBinop(QOpBoolAnd, query.having, e);
				} else {
					if (query.where == null)
						query.where = e;
					else
						query.where = SBinop(QOpBoolAnd, query.where, e);
				}
			}
		} else {
			if (query.where == null)
				query.where = cond;
			else
				query.where = SBinop(QOpBoolAnd, query.where, cond);
		}
	}
	
	public function setSelectCandidate(c:SqlExpr):Void
	{
		this.selectCandidate = c;
	}
	
	public function setAggregator(aggregateBy:Array<Variable>, aggregator:AggregatorFunction, column:Null<Variable>) : String
	{
		
		var vname = "n_" + (vnamesCount++);
		this.vnames.set(vname, { sourceAlias:sourceAlias } );
		
	}
	
	public function addOrderBy(v:Variable, order:SqlOrder) : Void
	{
		
	}
	
	//cada vez que começar um grupo group by, pega-se todas as variáveis a serem usadas e se coloca em outro contexto. Isso significa outros joins. Também se pega o primeiro join (nao, o join que já existe) (e em comum* - precisa-se pegar o em comum, não o primeiro) e se usa no select para fazer o join secundário entre os dois nested.
	public function beginGroupBy(entryPoint:SqlJoinSource):Null<GroupByContext>
	{
		//if there already is an aggregator, we need to nest the query
		if (this.query.aggregator == null)
		{
			//we add this placeholder aggregator just so the above check will fail on a nested group by case
			this.query.aggregator = { aggregatorFunc:null, columnVar:null, varName:null };
			
			return null;
		} else {
			//in order to get back to the SqlQuery currently in stack, we need to send it as an object
			var l = {
				lastQuery:this.query,
				lastJoins:this.joins,

				epAlias: null
			};

			l.epAlias = newSqlQuery(entryPoint, null);
			
			return l;
		}	
	}
	
	//como de uma variable eu consigo um join source?
	public function getJoinSource(v:Variable):SqlJoinSource
	{
		var alias = switch(v)
		{
			case VColumn(alias, _), VAlias(alias), VStruct(alias, _, _):
				alias;
			case VName(n):
				var n = vnames.get(n);
				n.sourceAlias;
		}
		
		return aliases.get(alias).source;
	}
	
	public function endGroupBy(g:Null<GroupByContext>)
	{
		if (g != null)
		{
			//we need to analyze all variables that are used by group by.
			//and select them before restoring the previous context
			var aliasSource = this.aliases.get(g.epAlias).source;
			
			var key = null;
			switch(aliasSource)
			{
				case STable(t):
					key = getPrimaryKeyField(t);
				default:
					error("A Group By statement should only be initiated by a table type. The sent type was " + aliasSource);
			}
			
			
			
			
			var selectBy = this.query.select;
			if (selectBy == null)
				selectBy = this.query.select = [];
			
			for (v in [VColumn(g.epAlias, key)].concat(usedVars))
			{
				selectBy.push({expr:SValue(VVariable(v)), name:null});
			}
			
			
		}
	}
	
}	

typedef GroupByContext =
{
	var lastQuery(default, null):SqlQuery;
	var lastJoins(default, null):Array<{alias:String, join:SqlJoin}>;
	var epAlias(default, null):String;
}